/* Peer - All public information concerning a peer.
   Copyright (C) 2003 Mark J. Wielaard

   This file is part of Snark.
   
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.
 
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
 
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

package org.klomp.snark;

import java.io.*;
import java.net.*;
import java.util.Arrays;
import java.util.Map;

import org.klomp.snark.bencode.*;

public class Peer implements Comparable
{
  // Identifying property, the peer id of the other side.
  private final PeerID peerID;

  private final byte[] my_id;
  private final MetaInfo metainfo;

  // The data in/output streams set during the handshake and used by
  // the actual connections.
  private DataInputStream din;
  private DataOutputStream dout;

  // Keeps state for in/out connections.  Non-null when the handshake
  // was successful, the connection setup and runs
  PeerState state;

  private boolean deregister = true;

  /**
   * Creates a disconnected peer given a PeerID, your own id and the
   * relevant MetaInfo.
   */
  public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo)
    throws IOException
  {
    this.peerID = peerID;
    this.my_id = my_id;
    this.metainfo = metainfo;
  }

  /**
   * Creates a unconnected peer from the input and output stream got
   * from the socket. Note that the complete handshake (which can take
   * some time or block indefinitely) is done in the calling Thread to
   * get the remote peer id. To completely start the connection call
   * the connect() method.
   *
   * @exception IOException when an error occurred during the handshake.
   */
  public Peer(final Socket sock, BufferedInputStream bis,
	      BufferedOutputStream bos, byte[] my_id, MetaInfo metainfo)
    throws IOException
  {
    this.my_id = my_id;
    this.metainfo = metainfo;

    byte[] id  = handshake(bis, bos);
    this.peerID = new PeerID(id, sock.getInetAddress(), sock.getPort());
  }

  /**
   * Returns the id of the peer.
   */
  public PeerID getPeerID()
  {
    return peerID;
  }

  /**
   * Returns the String representation of the peerID.
   */
  public String toString()
  {
    return peerID.toString();
  }

  /**
   * The hash code of a Peer is the hash code of the peerID.
   */
  public int hashCode()
  {
    return peerID.hashCode();
  }

  /**
   * Two Peers are equal when they have the same PeerID.
   * All other properties are ignored.
   */
  public boolean equals(Object o)
  {
    if (o instanceof Peer)
      {
	Peer p = (Peer)o;
	return peerID.equals(p.peerID);
      }
    else
      return false;
  }

  /**
   * Compares the PeerIDs.
   */
  public int compareTo(Object o)
  {
    Peer p = (Peer)o;
    return peerID.compareTo(p.peerID);
  }

  /**
   * Runs the connection to the other peer. This method does not
   * return until the connection is terminated.
   *
   * When the connection is correctly started the connected() method
   * of the given PeerListener is called. If the connection ends or
   * the connection could not be setup correctly the disconnected()
   * method is called.
   *
   * If the given BitField is non-null it is send to the peer as first
   * message.
   */
  public void runConnection(PeerListener listener, BitField bitfield)
  {
    if (state != null)
      throw new IllegalStateException("Peer already started");

    try
      {
	// Do we need to handshake?
	if (din == null)
	  {
	    Socket sock = new Socket(peerID.getAddress(), peerID.getPort());
	    BufferedInputStream bis
	      = new BufferedInputStream(sock.getInputStream());
	    BufferedOutputStream bos
	      = new BufferedOutputStream(sock.getOutputStream());
	    byte [] id = handshake(bis, bos);
	    byte [] expected_id = peerID.getID();
	    if (!Arrays.equals(expected_id, id))
	      throw new IOException("Unexpected peerID '"
				    + PeerID.idencode(id)
				    + "' expected '"
				    + PeerID.idencode(expected_id) + "'");
	  }
	
	PeerConnectionIn in = new PeerConnectionIn(this, din);
	PeerConnectionOut out = new PeerConnectionOut(this, dout);
	PeerState s = new PeerState(this, listener, metainfo, in, out);
	
	// Send our bitmap
	if (bitfield != null)
	  s.out.sendBitfield(bitfield);
    
	// We are up and running!
	state = s;
	listener.connected(this);
  
	// Use this thread for running the incomming connection.
	// The outgoing connection has created its own Thread.
	s.in.run();
      }
    catch(IOException eofe)
      {
	// Ignore, probably just the other side closing the connection.
	// Or refusing the connection, timing out, etc.
      }
    catch(Throwable t)
      {
	Snark.debug(this + ": " + t, Snark.ERROR);
	t.printStackTrace();
      }
    finally
      {
	if (deregister) listener.disconnected(this);
      }
  }

  /**
   * Sets DataIn/OutputStreams, does the handshake and returns the id
   * reported by the other side.
   */
  private byte[] handshake(BufferedInputStream bis, BufferedOutputStream bos)
    throws IOException
  {
    din = new DataInputStream(bis);
    dout = new DataOutputStream(bos);
    
    // Handshake write - header
    dout.write(19);
    dout.write("BitTorrent protocol".getBytes("UTF-8"));
    // Handshake write - zeros
    byte[] zeros = new byte[8];
    dout.write(zeros);
    // Handshake write - metainfo hash
    byte[] shared_hash = metainfo.getInfoHash();
    dout.write(shared_hash);
    // Handshake write - peer id
    dout.write(my_id);
    dout.flush();
    
    // Handshake read - header
    byte b = din.readByte();
    if (b != 19)
      throw new IOException("Handshake failure, expected 19, got "
			    + (b & 0xff));
    
    byte[] bs = new byte[19];
    din.readFully(bs);
    String bittorrentProtocol = new String(bs, "UTF-8");
    if (!"BitTorrent protocol".equals(bittorrentProtocol))
      throw new IOException("Handshake failure, expected "
			    + "'Bittorrent protocol', got '"
			    + bittorrentProtocol + "'");
    
    // Handshake read - zeros
    din.readFully(zeros);
    
    // Handshake read - metainfo hash
    bs = new byte[20];
    din.readFully(bs);
    if (!Arrays.equals(shared_hash, bs))
      throw new IOException("Unexpected MetaInfo hash");

    // Handshake read - peer id
    din.readFully(bs);
    return bs;
  }

  public boolean isConnected()
  {
    return state != null;
  }

  /**
   * Disconnects this peer if it was connected.  If deregister is
   * true, PeerListener.disconnected() will be called when the
   * connection is completely terminated. Otherwise the connection is
   * silently terminated.
   */
  public void disconnect(boolean deregister)
  {
    // Both in and out connection will call this.
    this.deregister = deregister;
    disconnect();
  }

  void disconnect()
  {
    PeerState s = state;
    if (s != null)
      {
	state = null;

	PeerConnectionIn in = s.in;
	if (in != null)
	  in.disconnect();
	PeerConnectionOut out = s.out;
	if (out != null)
	  out.disconnect();
      }
  }

  /**
   * Tell the peer we have another piece.
   */
  public void have(int piece)
  {
    PeerState s = state;
    if (s != null)
      s.havePiece(piece);
  }

  /**
   * Whether or not the peer is interested in pieces we have. Returns
   * false if not connected.
   */
  public boolean isInterested()
  {
    PeerState s = state;
    return (s != null) && s.interested;
  }

  /**
   * Sets whether or not we are interested in pieces from this peer.
   * Defaults to false. When interest is true and this peer unchokes
   * us then we start downloading from it. Has no effect when not connected.
   */
  public void setInteresting(boolean interest)
  {
    PeerState s = state;
    if (s != null)
      s.setInteresting(interest);
  }

  /**
   * Whether or not the peer has pieces we want from it. Returns false
   * if not connected.
   */
  public boolean isInteresting()
  {
    PeerState s = state;
    return (s != null) && s.interesting;
  }

  /**
   * Sets whether or not we are choking the peer. Defaults to
   * true. When choke is false and the peer requests some pieces we
   * upload them, otherwise requests of this peer are ignored.
   */
  public void setChoking(boolean choke)
  {
    PeerState s = state;
    if (s != null)
      s.setChoking(choke);
  }

  /**
   * Whether or not we are choking the peer. Returns true when not connected.
   */
  public boolean isChoking()
  {
    PeerState s = state;
    return (s == null) || s.choking;
  }

  /**
   * Whether or not the peer choked us. Returns true when not connected.
   */
  public boolean isChoked()
  {
    PeerState s = state;
    return (s == null) || s.choked;
  }

  /**
   * Returns the number of bytes that have been downloaded.
   * Can be reset to zero with <code>resetCounters()</code>/
   */
  public long getDownloaded()
  {
    PeerState s = state;
    return (s != null) ? s.downloaded : 0;
  }

  /**
   * Returns the number of bytes that have been uploaded.
   * Can be reset to zero with <code>resetCounters()</code>/
   */
  public long getUploaded()
  {
    PeerState s = state;
    return (s != null) ? s.uploaded : 0;
  }

  /**
   * Resets the downloaded and uploaded counters to zero.
   */
  public void resetCounters()
  {
    PeerState s = state;
    if (s != null)
      {
	s.downloaded = 0;
	s.uploaded = 0;
      }
  }
}
